"""
FFM + LR 点击率预测
"""


import numpy as np


def sigmoid(x):
    """
    sigmoid 函数
    y = (1 + tanh(x / 2)) / 2
    :param x:
    :return:
    """
    return 0.5 * (1 + np.tanh(0.5 * x))


class FFMNode:
    """
    通常x是高维稀疏向量，故可用链表来表示一个x，链表上的每个节点是个3元组（j, f, v）
    """
    # 按元组而非字典的方式来存储类的成员属性
    __slots__ = ['j', 'f', 'v']

    def __init__(self, j, f, v):
        """

        :param j: 特征 Feature(列) index 0 to n - 1
        :param f:  Field(行) index 0 to m - 1
        :param v:  value
        """
        self.j = j
        self.f = f
        self.v = v


class FFM:
    def __init__(self, m, n, k, eta, lambd):
        """

        :param m: number of fields(行)
        :param n: number of features(列)
        :param k: number of latent factors(潜在因子)
        :param eta: 学习率
        :param lambd: L2正则项中的参数lambd
        """
        self.m = m
        self.n = n
        self.k = k
        # 超参
        self.eta = eta
        self.lambd = lambd
        # 初始化三维权重矩阵：W~U(0, 1/sqrt(k))
        self.w = np.random.rand(n, m, k) / np.sqrt(k)
        # 初始化累积梯度平方和，AdaGrad时用，防止除以0
        self.G = np.ones(shape=(n, m, k), dtype=np.float)

    def phi(self, node_list):
        """
        特征组合式的线性加权求和
        FFM预测函数：sum(sum(v_{i, fj} * v_{j, fi} * x_i * x_j))
        :param node_list: 用链表存储x中的非0值
        :return:
        """
        z = 0.0
        for idx_a, node_a in enumerate(node_list):
            j1 = node_a.j
            f1 = node_a.f
            v1 = node_a.v
            for idx_b in range(idx_a + 1, len(node_list)):
                node_b = node_list[idx_b]
                j2 = node_b.j
                f2 = node_b.f
                v2 = node_b.v
                w1 = self.w[j1, f2]
                w2 = self.w[j2, f1]
                z += np.dot(w1, w2) * v1 * v2
        return z

    def predict(self, node_list):
        """
        将FFM的预测函数转换为sigmoid函数预测
        a = sigma(z) = 1 / (1 + e^{-z}) = 1 / (1 + e^{-phi})
        :param node_list: 用链表存储x中的非0值
        :return:
        """
        z = self.phi(node_list)
        return sigmoid(z)

    def sgd(self, node_list, y):
        """
        采用AdaGrad更新模型参数
        :param node_list: 用链表存储x中的非0值
        :param y: -1：负样本；1：正样本
        :return:
        """
        kappa = -y / (1 + np.exp(y * self.phi(node_list)))
        for idx_a, node_a in enumerate(node_list):
            j1 = node_a.j
            f1 = node_a.f
            v1 = node_a.v
            for idx_b in range(idx_a + 1, len(node_list)):
                node_b = node_list[idx_b]
                j2 = node_b.j
                f2 = node_b.f
                v2 = node_b.v
                c = kappa * v1 * v2

                g_j1_f2 = self.lambd * self.w[j1, f2] + c * self.w[j2, f1]
                g_j2_f1 = self.lambd * self.w[j2, f1] + c * self.w[j1, f2]
                # 计算各个维度上的梯度累积平方和
                self.G[j1, f2] += np.power(g_j1_f2, 2)
                self.G[j2, f1] += np.power(g_j2_f1, 2)

                # AdaGrad
                self.w[j1, f2] -= self.eta / np.sqrt(self.G[j1, f2]) * g_j1_f2
                self.w[j2, f1] -= self.eta / np.sqrt(self.G[j2, f1]) * g_j2_f1

    def train(self, sample_generator, max_echo, max_r2):
        """

        :param sample_generator: 样本生成器，每次yield(node_list, y)。x归一化
        :param max_echo: 最大迭代次数
        :param max_r2: 拟合系数r2达到阈值即可终止学习
        :return:
        """
        for itr in range(max_echo):
            print('iter: {}'.format(itr))
            y_sum = 0
            y_square_sum = 0.0
            err_square_sum = 0.0
            population = 0  # 样本总数
            for node_list, y in sample_generator:
                # 初始化
                y = 0.0 if y == -1 else y
                # 迭代更新参数
                self.sgd(node_list=node_list, y=y)
                # predict
                pre_y = self.predict(node_list=node_list)
                y_sum += y
                y_square_sum += np.power(y, 2)
                err_square_sum += np.power(y - pre_y, 2)
                population += 1
            # 方差
            var_y = y_square_sum - y_sum * y_sum / population
            """
            1、R平方的意义：模型解释的百分比
            2、R平方的计算：R^2 = 1 - 预测偏差的方差 / 真实值的方差 = 相关系数的平方
              1）、预测偏差的方差 = var(y - pre_y) = (y - pre_y)^2
              2）、真实值的方差 = var(y)
              3）、相关系数 = rho
              
            3、https://gitee.com/xiwanggit/book-math/blob/master/note/Think%20Stats.md
            """
            r2 = 1 - err_square_sum / var_y
            print('r2 = {}'.format(r2))
            if r2 > max_r2:
                break

    def save_model(self, out_file):
        """

        :param out_file:
        :return:
        """
        np.save(out_file, self.w)

    def load_model(self, input_file):
        """

        :param input_file:
        :return:
        """
        self.w = np.load(input_file)
